<template>
  <div :class="['JsonFrom', appStore.isDeep ? 'deep' : 'light']">
    <div>
      <div v-show="!isResult" class="toggle">
        <div class="label">{{ label }}</div>
        <MyTab
          v-model:value="state.type"
          :disabled="editable"
          :options="[
            { label: '文本', value: 'json' },
            { label: '选择', value: 'form' },
          ]"
        />
      </div>
      <div v-if="jsonLabel && !(isResult || state.type !== 'json')" class="groupLabel">
        {{ jsonLabel }}
      </div>
      <div
        ref="$json"
        class="json-container relative mx-auto mb-20 mt-64 h-400"
        :class="{ noShow: isResult || state.type !== 'json' }"
      >
        <div class="control" @click="toggleEditable">
          <img
            class="h-16 w-16"
            :src="
              editable
                ? appStore.isDeep
                  ? deepSaveImg
                  : lightSaveImg
                : appStore.isDeep
                ? deepEditImg
                : lightEditImg
            "
          />
          {{ editable ? '保存' : '编辑' }}
        </div>
      </div>
      <div v-show="isResult || state.type === 'form'" class="form-container">
        <div v-if="isResult" class="mx-auto mb-20 flex-col items-end">
          <n-button class="download" color="#6D61E3" @click="handleDownload">
            <n-icon :size="18" :component="FileDownloadOutlined" style="margin-right: 4px"></n-icon>
            下载
          </n-button>
        </div>

        <n-form
          v-if="isInit"
          ref="$form"
          :model="formData"
          label-placement="left"
          label-align="right"
          :label-width="140"
          require-mark-placement="left"
          :show-require-mark="isResult ? false : undefined"
          :class="{ lastLine: line }"
        >
          <template v-for="schema in schemas" :key="schema.field || schema.groupLabel">
            <div
              v-if="
                schema.groupLabel && (schema.isShow ? schema.isShow({ isResult, formData }) : true)
              "
              class="groupLabel"
            >
              {{ schema.groupLabel }}
            </div>
            <n-form-item
              v-else-if="schema.isShow ? schema.isShow({ isResult, formData }) : true"
              :label="schema.label"
              :path="schema.field"
              :rule="schema.rule"
            >
              <!--NCheckbox-->
              <template v-if="schema.component === 'NCheckbox'">
                <n-checkbox-group
                  v-model:value="formData[schema.field]"
                  v-bind="schema.componentProps"
                  :disabled="
                    schema.isDisabled
                      ? schema.isDisabled({ isResult, formData })
                      : schema.componentProps.disabled
                  "
                  @update:value="handleFormChange"
                >
                  <n-space>
                    <n-checkbox
                      v-for="item in schema.componentProps.options"
                      :key="item.value"
                      :value="item.value"
                      :label="item.label"
                    />
                  </n-space>
                </n-checkbox-group>
              </template>
              <!--NRadioGroup-->
              <template v-else-if="schema.component === 'NRadioGroup'">
                <n-radio-group
                  v-model:value="formData[schema.field]"
                  v-bind="schema.componentProps"
                  :disabled="
                    schema.isDisabled
                      ? schema.isDisabled({ isResult, formData })
                      : schema.componentProps.disabled
                  "
                  @update:value="handleFormChange"
                >
                  <n-space>
                    <n-radio
                      v-for="item in schema.generateOptions
                        ? schema.generateOptions({ isResult, formData })
                        : schema.componentProps.options"
                      :key="item.value"
                      :value="item.value"
                      :label="item.label"
                    />
                  </n-space>
                </n-radio-group>
              </template>
              <template v-else-if="schema.component === 'AsyncSelect'">
                <AsyncSelect
                  v-bind="schema.componentProps"
                  v-model:value="formData[schema.field]"
                  :disabled="
                    schema.isDisabled
                      ? schema.isDisabled({ isResult, formData })
                      : schema.componentProps.disabled
                  "
                  :extra-params="
                    schema?.componentProps?.getExtraParams
                      ? schema?.componentProps?.getExtraParams(formData)
                      : {}
                  "
                  @update:value="handleFormChange"
                  @async-load="(list) => handleLoad(schema, list)"
                />
              </template>
              <template v-else-if="schema.component === 'MyTags'">
                <MyTags
                  v-bind="schema.componentProps"
                  v-model:modelValue="formData[schema.field]"
                  :extra-params="
                    schema?.componentProps?.getExtraParams
                      ? schema?.componentProps?.getExtraParams(formData)
                      : {}
                  "
                  :disabled="
                    schema.isDisabled
                      ? schema.isDisabled({ isResult, formData })
                      : schema.componentProps.disabled
                  "
                  :only-select="
                    schema.isOnlySelect
                      ? schema.isOnlySelect({ isResult, formData })
                      : schema.componentProps.onlySelect
                  "
                  @update-model-value="handleFormChange"
                  @async-load="(list) => handleLoad(schema, list)"
                />
              </template>
              <!-- other -->
              <component
                :is="naive[schema.component]"
                v-else
                v-bind="schema.componentProps"
                v-model:value="formData[schema.field]"
                :disabled="
                  schema.isDisabled
                    ? schema.isDisabled({ isResult, formData })
                    : schema.componentProps.disabled
                "
                @update:value="handleFormChange"
              />
            </n-form-item>
          </template>
        </n-form>
      </div>
      <div v-show="!isResult" class="relative mx-auto mb-10 w-350 flex items-center">
        <FileUpload
          v-model="fileList"
          :accept="['json']"
          :file-size="fileSize"
          :custom-request="uploadJson"
        />
      </div>
    </div>
  </div>
</template>
<script setup lang="jsx">
import { pickBy } from 'lodash-es'
import { useAppStore } from '@/store'
import lightSaveImg from '@/assets/images/light-save.png'
import lightEditImg from '@/assets/images/light-edit.png'
import deepSaveImg from '@/assets/images/deep-save.png'
import deepEditImg from '@/assets/images/deep-edit.png'
import FileSaver from 'file-saver'
import FileDownloadOutlined from '@vicons/material/FileDownloadOutlined'
import * as naive from 'naive-ui'
import JsonEditor from 'jsoneditor'
import 'jsoneditor/dist/jsoneditor.min.css'
import { objectMergLeft } from '@/utils/common'
defineOptions({
  name: 'JsonForm',
})
const appStore = useAppStore()
const props = defineProps({
  label: {
    type: String,
    default: '',
  },
  schemas: {
    type: Array,
    default: () => [],
  },
  fileSize: {
    type: Number,
    default: 500,
  },
  isResult: {
    type: Boolean,
    default: false,
  },
  jsonLabel: {
    type: String,
    default: null,
  },
  line: {
    type: Boolean,
    default: false,
  },
})
watch(
  () => props.isResult,
  () => {
    nextTick(() => {
      setJson(format('json', formData.value))
    })
  }
)
const emits = defineEmits(['submit', 'change', 'options-load'])
const state = reactive({
  type: 'json',
})
//json容器
const $json = ref(null)
//json编辑器实例
let editor = null
// 初始化json
function initJson() {
  // console.log(generateSchema())
  editor = new JsonEditor($json.value, {
    mode: 'code',
    mainMenuBar: false,
    language: 'zh-CN',
    schema: {
      type: 'object',
      properties: generateSchema(),
    },
    onEditable() {
      return editable.value
    },
  })
}
// 依据模板生成json校验schema
function generateSchema() {
  return Object.fromEntries(
    props.schemas
      .filter((item) => !item.groupLabel)
      .map((item) => {
        let type = null
        if (item.component === 'NInputNumber') {
          type = 'number'
        } else if (item?.componentProps?.multiple) {
          type = 'array'
        } else {
          type = 'string'
        }
        return [item.field, { type: [type, 'null'] }]
      })
  )
}
// 销毁json
function destroyJson() {
  editor?.destroy()
  editor = null
}

function filterShow(raw) {
  return pickBy(raw, (_, key) => {
    const current = props.schemas.find((item) => item.field === key)
    if (current) {
      return current.isShow
        ? current.isShow({ isResult: props.isResult, formData: formData.value })
        : true
    } else {
      return false
    }
  })
}

// 设置json
function setJson(data, isMerge = false) {
  // 展示的数据是中文
  const raw = !isMerge ? data : objectMergLeft(editor.get(), data)
  // console.log('raw:', raw, filterShow(raw))
  editor.set(filterShow(raw))
}
// 校验json
async function validateJson() {
  let jsonData = null
  try {
    jsonData = editor.get()
  } catch (error) {
    return `json格式异常!`
  }

  const filterData = filterShow(formData.value)
  // 校验字段是否缺失
  for (const key in filterData) {
    if (!(key in jsonData)) {
      return `字段${key}缺失！`
    }
  }
  // 校验字段是否冗余

  for (const key in jsonData) {
    if (!(key in filterData)) {
      return `不合法字段${key}!`
    }
  }
  // 校验类型错误
  const res = await editor.validate()
  if (res.length) {
    return `字段${res[0].dataPath.substring(1)}类型错误,应为${res[0].schema}类型!`
  }
}
// 是否可以编辑json
const editable = ref(false)
// 切换是否可编辑
async function toggleEditable() {
  if (editable.value) {
    //校验
    const errorMsg = await validateJson()
    if (errorMsg) {
      $message.warning(errorMsg)
      return
    }
    setForm(format('form', editor.get()), true)
    editable.value = false
    // nextTick(() => {
    //   emits('change', formData.value)
    // })
  } else {
    editable.value = true
  }
  editor.setMode('text')
  editor.setMode('code')
}

// 表单ref
const $form = ref(null)
// 实际数据
const formData = ref({})

// 表单发生变化
function handleFormChange() {
  console.log('用户手动修改表单')
  setJson(format('json', formData.value))
  // emits('change', formData.value)
}
// false 覆盖， true 合并
function setForm(data, isMerge = false) {
  // console.log('form', data)
  formData.value = !isMerge ? data : objectMergLeft(formData.value, data)
}
/***
 * form数据转换为json数据，（例如：json中展示中文，form中存储id）
 * mode: form|json
 */
function format(mode, data) {
  if (!['form', 'json'].includes(mode)) {
    throw new Error('转换模式异常！')
  }
  return Object.fromEntries(
    Object.entries(data)
      .map(([field, value]) => {
        const current = props.schemas.find((item) => item.field === field)
        if (!current) return false
        if (current.enableFormat) {
          let labelKey = ''
          let valueKey = ''
          if (mode === 'json') {
            labelKey = current?.componentProps?.labelKey || 'label'
            valueKey = current?.componentProps?.valueKey || 'value'
          } else {
            labelKey = current?.componentProps?.valueKey || 'value'
            valueKey = current?.componentProps?.labelKey || 'label'
          }
          const formatData = (val) =>
            ((optionsMap[field] || []).find((item) => item[valueKey] === val) || {})[labelKey] ||
            null
          if (current?.componentProps?.multiple) {
            return [field, value?.length ? value.map((it) => formatData(it)) : value]
          } else {
            return [field, formatData(value)]
          }
        } else {
          return [field, value]
        }
      })
      .filter((val) => val)
  )
}
const optionsMap = reactive({})
function handleLoad({ field, componentProps, enableFormat }, list = []) {
  // console.log(field, list, formData.value[field])
  //设置异步数据项
  if (enableFormat === 'async') {
    optionsMap[field] = list
    if (
      Object.entries(optionsMap).length === props.schemas.filter((item) => item.enableFormat).length
    ) {
      emits('options-load')
    }
    const formatData = (val) =>
      (list.find((item) => item[componentProps?.valueKey] === val) || {})[
        componentProps.labelKey
      ] || null
    let res = null
    if (componentProps.multiple) {
      res = formData.value[field].length
        ? formData.value[field].map((v) => formatData(v))
        : formData.value[field]
    } else {
      res = formatData(formData.value[field])
    }
    setJson({ [`${field}`]: res }, true)
  }
}

// 初次渲染是否完成
const isRender = defineModel('isRender', {
  type: Boolean,
  default: false,
})

// 初始数据init完成
const isInit = defineModel('isInit', {
  type: Boolean,
  default: false,
})
function initData() {
  isRender.value = false
  isInit.value = false
  const data = Object.fromEntries(
    props.schemas
      .filter((item) => !item.groupLabel)
      .map(({ field, componentProps, enableFormat }) => {
        let res = null
        if (
          formData.value[field] === null ||
          formData.value[field] === undefined ||
          (Array.isArray(formData.value[field]) && formData.value[field]?.length === 0)
        ) {
          res = componentProps?.defaultValue === undefined ? null : componentProps?.defaultValue
        } else {
          res = formData.value[field]
        }
        // 设置同步转换数据项
        if (enableFormat === 'sync') {
          optionsMap[field] = componentProps.options || []
        }
        return [field, res]
      })
  )
  setData(data, 'form', false)
  nextTick(() => {
    isRender.value = true
    isInit.value = true
  })
}
function setData(data, type, isMerge = false) {
  if (type === 'form') {
    setForm(data, isMerge)
    setJson(format('json', data), isMerge)
  }
  if (type === 'json') {
    setJson(data, isMerge)
    setForm(format('form', data), isMerge)
  }
}

function validateForm(callBack = () => {}) {
  if (editable.value) {
    return $message.warning(`正在编辑json，请先保存!`)
  }
  $form.value.validate((errors) => {
    if (!errors) {
      callBack(filterShow(formData.value), editor.get())
    } else {
      if (state.type === 'json') {
        state.type = 'form'
      }
    }
  })
}

watch(
  () => props.schemas,
  () => {
    initData()
  },
  {
    deep: true,
  }
)
onMounted(() => {
  initJson()
  initData()
})
onUnmounted(() => {
  destroyJson()
})

//文件上传相关，上传json文件成功后读取值
const fileList = ref([])
const fileSizeByte = computed(() => 2 ** 10 * props.fileSize)
function uploadJson({ file: { file } }) {
  if (file.size > fileSizeByte.value) {
    $message.warning(`单个文件不能超过${props.fileSize}kb!`)
    return false
  }
  isRender.value = false
  let reader = new FileReader()
  reader.readAsText(file)
  reader.onload = (e) => {
    try {
      const res = JSON.parse(e.target.result)
      setData(res, 'json', true)
      nextTick(() => {
        fileList.value = [{ fileUrl: Date.now(), fileOriginalName: file.name }]
        isRender.value = true
      })
    } catch (error) {
      console.log(error)
      nextTick(() => {
        isRender.value = true
      })
      $message.error(`文件内容异常!`)
    }
  }
}
function handleDownload() {
  validateForm(() => {
    const blob = new Blob([JSON.stringify(editor.get())], {
      type: 'application/json',
    })
    FileSaver.saveAs(blob, 'config.json')
  })
}

function setFormData(data) {
  setForm(data, true)
}
function setJsonData(data) {
  isRender.value = false
  setData(data, 'json', true)
  nextTick(() => {
    isRender.value = true
  })
}
defineExpose({
  validateForm,
  setFormData,
  setJsonData,
  format,
})
</script>
<style lang="scss" scoped>
.JsonFrom {
  .toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
  }
  .json-container.noShow {
    width: 0;
    height: 0;
    overflow: hidden;
    opacity: 0;
    margin: 0;
  }
  .json-container {
    .control {
      position: absolute;
      right: 0;
      top: -48px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 4px;
      width: 80px;
      height: 32px;
      cursor: pointer;
      font-size: 14px;
    }
  }

  &.light {
    .label {
      font-size: 28px;
      color: #140d35;
      font-weight: 500;
    }
    :deep(.n-button.download) {
      box-shadow: 0px 3px 8px 0px rgba(109, 97, 227, 0.47);
      font-weight: 200 !important;
    }
    :deep(.n-form.lastLine) {
      padding-bottom: 16px;
      border-bottom: 1px solid #e7eaf0;
      margin-bottom: 24px;
    }
    .control {
      background: #ffffff;
      border-radius: 6px;
      border: 1px solid #d9dee9;
      color: #060312;
    }
  }
  &.deep {
    .label {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 20px;
      color: #4ddef4;
      &::before {
        content: '';
        display: block;
        width: 30px;
        height: 22px;
        background: url(@/assets/images/deep-title-icon.png);
      }
    }
    :deep(.n-form.lastLine) {
      padding-bottom: 16px;
      border-bottom: 1px solid rgba(43, 161, 255, 0.3);
      margin-bottom: 24px;
    }
    .control {
      border: 1px solid #2ba1ff;
      color: #2ba1ff;
    }
    :deep(.n-button.download) {
      background: url(@/assets/images/deep-solid-btn-bg-larger.png) center/100% no-repeat !important;
      font-weight: 200 !important;
      width: 88px;
      height: 38px;
    }
  }
}
</style>
